終於來到鐵人賽的後半段囉~給自己一點鼓勵!
前半段都在理解和複習Vue的一些使用上的觀念和陷阱,接下來希望能複習一些基本JavaScript觀念,讓我們可以慢慢推進到更進階的JavaScript設計模式等。
之前已經有處理一些重複性邏輯時,會使用所謂的組合式函式(composable)
來達成複用,其中衍生的觀念式是從JavaScript工廠模式(Factory Pattern)
,是一個很基礎但常出現的設計模式,只是不太常察覺。
順便熟悉一下另一個類別建構子(class)
,兩者同樣能達成共用物件或擴充的目的。今天就稍微複習一下,兩者沒有絕對好或壞,針對專案需求可以選擇覺得合適的設計方案就行。~
類別建構子
和工廠函式
的差別閉包(closure)
觀念再複習私有變數(private)
延展和繼承(extend and inheritance)
,差異性在哪裡?類別建構子
和工廠函式
的差別類別建構子(class)
是 ECMAScript 2015 (ES6) 規範中的 JavaScript 新增的特性。類別提供了一種就像是物件導向程式設計(OOP)
方法,對於使用 Java 或 C++ 等語言的開發人員來說,會感到很熟悉,雖然本質上JavaSript 是沒有class系統,是用原型繼承(prototype inheritance)
來模擬。
class關鍵字
來定義constructor
用於初始化新實例的內部變數原型(prototype)
中this
來指向建構子的變數(非同步稍微注意指向問題)extend
關鍵字來實現繼承class Rectangle {
// 建構函式定義初始化物件綁定資料
constructor(height, width) {
this.height = height;
this.width = width;
}
// 內部定義方法
getArea() {
return this.height * this.width;
}
}
const rect = new Rectangle(10, 20);
console.log(rect.getArea()); // 200
工廠函式(factory function)
是一個在ES6之前還沒有類別出現時常見的設計模式,是將物件的創建過程封裝在函式中,並透過該函式來生成新的物件,當然也可以只返回一個特定變數
,相較於類別在回傳值上有更高靈活性。
我們可以用工廠函式於創建對象的過程中不直接使用new 關鍵字
來實例化對象,可以通過函數來生成和返回對象。
不使用new關鍵字創建
,採用熟悉的函式呼叫(invoke)function createRectangle(height, width) {
return {
height,
width,
getArea() {
return this.height * this.width;
}
}
}
const rect = createRectangle(10, 20);
console.log(rect.getArea()); // 200
先看一個簡單的應用,為什麼呼叫 makeAdding 函式並輸入參數5,最終可以得到返回值7呢
?
function makeAdding (firstNumber) {
const first = firstNumber;
return function resulting (secondNumber) {
const second = secondNumber;
return first + second;
}
}
const add5 = makeAdding(5);
console.log(add5(2)) // logs 7
函數makeAdding接受一個參數 ,firstNumber宣告一個first值為 的常數firstNumber,並傳回另一個函數。
當參數2傳遞給傳回的函式add5時,它會傳回將先前傳遞的數字5與現在傳遞的數字2相加的結果,也就是7,在JavaScript中函式會形成閉包(closure)。
閉包是指函式和其被宣告時所在的周圍狀態(也稱為語彙環境lexiccal scope)的組合,這個周圍狀態包含了函式建立時在作用域內的所有局部變數
。
當 makeAdding 函式執行(execution phase)時,add5 是指向makeAdding函式,其中包含了變數 first,JS為了能夠正確運行並捕捉對應的變數資料來進行運算,透過宣告時期(creation phase)已經建立好的語彙環境
,去搜尋變數 first被賦予的值,也就是5。
工廠函式的一大優點是能夠利用閉包(closure)
來創建「私有」變數和函式,這些變數和函式只能在工廠函式內部訪問,而無法從外部直接存取。
可以很直觀透過返回值去控制模組需要公開的方法和屬性
,這使得模組的使用者只能使用經過我們設計過的API介面(interface),增強了程式碼的可靠性和易用性。
function createUser(name) {
// 私有變數
let privateId = Math.random().toString(36).substr(2, 9);
// 私有函式
function generateGreeting() {
return `Hello, ${name}! Your ID is ${privateId}.`;
}
// 公開的變數和函式
const discordName = "@" + name;
return {
name,
discordName,
getGreeting: generateGreeting, // 公開方法訪問私有函式
getId: () => privateId, // 公開方法訪問私有變數
};
}
// 使用工廠函式
const user = createUser("Alice");
// 無法直接訪問私有變數和函式
console.log(user.privateId); // undefined
console.log(user.generateGreeting); // undefined
當然你會想說物件建構子做不到實現私有屬性和方法嗎,其實好像可以, ES2022 引入了私有屬性語法(使用 # 前綴)
,讓我們可以在 class 中定義真正的私有屬性和方法:
class User {
// 私有變數使用 # 前綴
#privateId;
#privateMethod() {
console.log("This is a private method.");
}
constructor(name) {
this.name = name;
this.discordName = "@" + name;
this.#privateId = Math.random().toString(36).substr(2, 9);
}
// 公開方法可以訪問私有屬性和方法
getId() {
return this.#privateId;
}
publicMethod() {
console.log("This is a public method.");
this.#privateMethod(); // 呼叫私有方法
}
}
const user = new User("Alice");
// 嘗試訪問私有屬性或方法會失敗
console.log(user.#privateId); // SyntaxError: Private field '#privateId' must be declared in an enclosing class
console.log(user.#privateMethod); // SyntaxError: Private field '#privateMethod' must b
類別建構子使用extend
滿方便的,在新的物件實例使用起來也滿直觀~
但目前看到比較嚴重的缺點是方法屬性同名的話,後代會有改寫並覆蓋的情況,在實務上應該比較不希望這種狀況產生,如果後面的高階模組一直蓋掉低階模組,使用起來好像會混淆。
需要做一層屬性定義defineProperty
,不然會後代新方法覆蓋掉原本父設計好函式的風險。
class Animal {
constructor(name) {
this.name = name;
}
speak() {
console.log(`${this.name} makes a noise.`);
}
}
// 凍結 speak 方法,不可以覆寫
Object.defineProperty(Animal.prototype, 'speak', {
writable: false,
configurable: false
});
class Dog extends Animal {
constructor(name) {
super(name);
}
speak() {
console.log(`${this.name} barks.`);
}
}
const dog = new Dog(‘Rex‘);
dog.speak(); // Rex barks.
工廠函式上面有介紹過會返回一個新的物件
,而不需要使用 new 關鍵字
。
如果希望做到可以有類似class父子類繼承作用的,需要使用 Object.assign
或展開運算子(spread operator)
將原本的函式中的方法複製到新物件,我記得之前物件操作方法章節有提到。
不過個人感覺使用上不像類別extend
,有較清楚的物件繼承上下關係,反而比較像混合模式(Mixin pattern)
,將各種屬性集中到同一物件,因為同樣會有作用域命名(named scope)
上的問題,合併順序就滿重要,因為後面同樣的屬性會覆蓋掉前面重複名稱。
。
// 製作一個基本動物函式
const animalMethods = {
speak() {
console.log(`${this.name} makes a noise.`);
}
};
const runnerMethods = {
run() {
console.log(`${this.name} is running.`);
}
};
// 工廠函式繼承多個方法
function createAnimal(name) {
return {
name,
...animalMethods,
...runnerMethods // 使用展開運算子將多個方法合併
};
}
// 創建一隻動物
const myAnimal = createAnimal('Cheetah');
myAnimal.speak(); // Cheetah makes a noise.
myAnimal.run(); // Cheetah is running.
const pobby = createAnimal(Cheetah);
// 檢查 pobby 的原形
console.log(Object.getPrototypeOf(pobby)); // 輸出: Object.prototype
console.log(pobby instanceof createAnimal); // 輸出: false
工廠函式返回的物件沒有利用 JavaScript 的原型鏈來共享方法
,每個物件都是獨立的,方法是直接附加在物件上的。這些方法並不共享,而是每個物件有自己的一份副本。這和 class 不同,class 可以利用原型來共享方法。
在類別class
中,當你定義方法時,這些方法會被添加到類別的原型鏈上共享。也就是說,所有由這 class 創建的物件實例都會共享同一個方法,而不是每個物件都有自己的副本,不過在目前設備硬體都很充分情況下,大部分開發效能差異性並不大。
使用類別class
的方式更符合熟悉物件導向概念的開發者,尤其當設計關係上出現,需要多層次繼承時會更具可讀性和可維護性
。
特性 | 工廠函式 | 類別class |
---|---|---|
方法位置 | 每個實例都有自己的方法副本 | 方法位於原型鏈上,所有實例共享同一個方法 |
記憶體效率 | 每次創建都複製方法,可能會浪費記憶體(很大量的話) | 方法共享,記憶體使用更有效 |
代碼風格與結構 | 更具靈活性,可自己定義返回的物件 | 適合更複雜的物件結構,符合 OOP 模式 |
擴展性 | 靈活,但擴展較複雜,比較沒上下關係,偏混合式(mixin)風格 | 直接使用 extends 繼承,明確層級關係,可讀性會比較高 |